Ascii,Gbk,Unicode字符集和UTF-8,UTF-16编码详解

   之前一直使用第三方的NPM包对文件上传做处理,上周在了解具体的实现原理中,遇到了编码方面的问题
,然后又去了解了编码。

Ascii,GBK,Unicode 字符集

Ascii

  Ascii(美国信息交换标准代码),它是一套电脑编码系统,使用连续的字节状态来表示英文文字。Ascii是一种单字节的编码系统,以至于最多只能有2的八次方种状态。Ascii是美国人的标准,并不能把全世界的语言都表示出来。最后出现了很多其他的编译系统,比如我们的GBK。

GBK

  为了适应中国自己的需求,我们发明了一种GB2312编码:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(高字节)从0xA1用到0xF7,后面一个字节(低字节)从0xA1到0xFE。但是中国的汉字太多了,还是没法满足我们的需要。于是在GB2312编码上进行扩展,只要第一个字节表示大于127就表示汉字,这种编码方式就是GBK。

Unicode

  当时各国都在各种搞一套,导致编码标准不统一。这时候国际标谁化组织开始统一标准,Unicode出现了。Unicode中对Ascii的127以下的编码方式保持不变,其他的重新进行编码。其中,Unicode目前普遍采用的是UCS-2(每2个字节表示一个符号),这导致之前使用Ascii表示的符号多出了1个字节。Unicode并不完美。直到互联网的出现,为解决unicode如何在网络上传输的问题UTF出现,这也是我们要重点讲解的编码。

Ascii,GBK,Unicode 和 UTF,UCS 区别

  Ascii,GBK,Unicode准确来说应该叫做字符集,对每个字符使用对应的唯一的代码值来表示,并没有规定使用多少个字节来表示。UTF,UCS是基于Unicode字符集的编码方式,使用对应的字节来表示字符。

UTF 编码

  早期Unicode版本中,UTF分为UTF-8,UTF-16,后来又有了UTF-32。UTF-8并不是表示一个字符用一个字节表示,UTF-8使用可变的字节数来表示一个字符。根据字节中开头的bit标志来识别使用1~4个字节来表示一个字符。UTF-16表示使用固定的2个字节来表示任何的字符。UTF-32使用4个字节表示任意字符。

UTF-8 编码详解

  UTF-8是可变的字节编码,规则如下:

1
2
3
4
0xxxxxxx
110xxxxx 10xxxxxx
1110xxxx 10xxxxxx 10xxxxxx
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

  可以看到,UTF-8中开头的bit是标志信息。除去这些标志信息,UTF-8中一个字节只能表是2的7次方(128)个字符,两个字节只能表示2的11次方(2048)个字节,三个字节只能表示2的16次方(65536)个字节,四个只能表示2的21次方(2097152)个字节。其实在早期UTF-8可以到达6个字节序列,后来被RFC 3629重新规范,只能使用原来Unicode定义的区域而Unicode6.1定义范围为0到0x10FFFF,也就是0到2的21次方。UFT-8就只能到4个字节序列了。

UTF编码表示字符

  因为中、日、韩的三种文字占用了Unicode(UCS-2)中0x3000到0x9FFF的代码值,所以需要使用3个字节的utf-8来显示,而只需要2个字节的uft-16显示。

1
2
3
4
5
6
7
8
比如:玉 (29577)
UTF-8 | UFT-16
e7 8e 89 | 73 89
11100111 10001110 10001001 | 01110011 10001001
比如:a 97
61 | 00 61
01100001 | 00000000 01100001

  可以看出在Unicode(UCS-2)中代码值为29577,UTF-8使用3个字节表示,UFT-16中使用了2个字节表示。a代码值为97,UTF-8使用1个字节表示,UFT-16中使用了2个字节表示。

UTF-8和UTF-16优劣势

  我们从这里可以看出:

  1. UTF-8的优势在于对英文编码和Ascii一样只用一个字节表示,而UTF-16需要浪费一个字节。
  2. 而对应汉字UTF-8需要使用3个字节表示,而UTF-16就能节约一个字节。
  3. UTF-16不能像UTF-8对小于127代码值的字符友好的兼容Unicode。

判断UTF-8,UTF-16编码

   很多场景下我们需要去判断字符编码格式,比如文件上传。

方法一

  有的文件会自动添加BOM到文件头,我们可以通过它来判断文件编码格式。但是,首先不能保证所有的文件都带有BOM,其次BOM头添加导致无法实现对ASCII的兼容。
  BOM标志:

1
2
3
4
5
EF BB BF    UTF-8
FE FF     UTF-16/UCS-2, little endian
FF FE     UTF-16/UCS-2, big endian
//说明 UTF-16 对字符具有2种编码方式,一种是从左向右,一种从右向左

简单判断代码:

1
2
3
4
5
6
7
8
9
function byteType(bytes){
if(bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] = 0xBF){
// UTF-8
}else if(bytes[0] == 0xFE && bytes[1] = 0xFF){
// UTF-16LE
}else if((bytes[0] == 0xFF && bytes[1] = 0xFE)){
// UTF-16BE
}
}

方法二

  通过对文件的每一个字节进行判断,来区分UTF编码,但是这个也不能保证完全准确。
比如: 文件中所有字符的UTF-16编码都为110xxxxx 10xxxxxx,就无法区分UTF编码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
function isUTF8(bytes){
var i = 0;
while (i < bytes.length) {
if ((// ASCII 0xxxxxxx
0x00 <= bytes[i] && bytes[i] <= 0x7F
)) {
i += 1;
continue;
}
if (( // 110xxx1x 10xxxxxx
(0xC2 <= bytes[i] && bytes[i] <= 0xDF) &&
(0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF)
)) {
i += 2;
continue;
}
if (
( //1110xxxx 101xxxxx 10xxxxxx
bytes[i] == 0xE0 &&
(0xA0 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
(0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF)
) || (
((0xE1 <= bytes[i] && bytes[i] <= 0xEC) ||
bytes[i] == 0xEE ||
bytes[i] == 0xEF) &&
(0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
(0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF)
) || (
bytes[i] == 0xED &&
(0x80 <= bytes[i + 1] && bytes[i + 1] <= 0x9F) &&
(0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF)
)
) {
i += 3;
continue;
}
if (
( //11110xxx 10x1xxxx 10xxxxxx 10xxxxxx
bytes[i] == 0xF0 &&
(0x90 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
(0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) &&
(0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF)
) || (
(0xF1 <= bytes[i] && bytes[i] <= 0xF3) &&
(0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
(0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) &&
(0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF)
) || (
bytes[i] == 0xF4 &&
(0x80 <= bytes[i + 1] && bytes[i + 1] <= 0x8F) &&
(0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) &&
(0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF)
)
) {
i += 4;
continue;
}
return false;
}
}

注意:在判断时记住,无论一个字符使用多少个字节序列表示,都无法小于对应的代码值。比如:
判断2个字节的时候使用0xC2 <= bytes[i] && bytes[i] <= 0xDF) && (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF),而根据规则(110xxxxx 10xxxxxx)好像应该使用0xC0 <= bytes[i] && bytes[i] <= 0xDF) && (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF)但是UTF-8中2个字节表示的代码值应该大于2的11次方,所以第一种才是正确的。